/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2022年11月29日
 * V4.0
 */
package com.jphenix.driver.udp;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;

import com.jphenix.standard.docs.BeanInfo;
import com.jphenix.standard.docs.ClassInfo;

/**
 * UDP客户端
 * 
 * 1. 可以用来测试第三方的UDP服务器，发送普通消息
 * 2. 可以测试UDPServer本地服务
 * 
 * 测试UDPServer服务需要以下几个步骤：
 * 
 * 1. 启动UDPServer服务
 * 2. 定义好监听端口,sysCode值
 * 3. 在服务端先构建token（随机字符串）然后调用server中的regist方法注册生成token会话
 * 4. 用这个token值和sysCode值在当前客户端测试调用服务端
 * 
 * 调用服务端返回的信息是5个元素的字符串数组：
 * 0 消息类型（1个字节） R重定向消息 M普通消息 Z无内容消息 E错误异常消息
 * 1 sysCode
 * 2 token
 * 3 msgCode
 * 4 msg
 * 
 * com.jphenix.driver.udp.UDPClient
 * 
 * @author mbg
 * 2022年11月29日
 */
@ClassInfo({"2022-11-29 14:52","UDP客户端"})
@BeanInfo({"udpclient","0","","","1"})
public class UDPClient {

	//#region 常量声明区
	// 报文长度：标准MTU值为576字节,最好将UDP的数据长度控件在548字节(576-8-20)以内.（8为UDP头。20为IP头）
	// 最大报文长度
	public final static int MAX_LENGTH = 548;
	
	private final static byte MSG_SEPARATOR = 0x0B; // 消息段分隔符 0x0B '|'
	//#endregion
	
	//#region 变量声明区
	private DatagramChannel channel = null; // 通信类
	private Charset         charset = StandardCharsets.UTF_8; //编码转换器
	//#endregion
	
	//#region 构造函数
	/**
	 * 构造函数
	 * @author mbg
	 */
	public UDPClient() {
		super();
	}
	//#endregion
	
	//#region sendMsg(msg,port,resBufferSize) 发送数据到本地，并获取返回值
	/**
	 * 发送数据到本地，并获取返回值
	 * @param msg            发送消息内容
	 * @param port           目标端口号
	 * @param address        目标地址
	 * @param resBufferSize  接收消息缓存大小
	 * @return               接收到的消息
	 * @throws Exception     异常
	 * 2022年11月29日
	 * @author mbg
	 */
	public String sendMsg(String msg,int port,String address,int resBufferSize) throws Exception {
		if(msg==null) {
			return null;
		}
		ByteBuffer res = send(ByteBuffer.wrap(msg.getBytes()),port,address,resBufferSize);
		if(res==null) {
			return null;
		}
		// 获取数据
		res.flip();
		return charset.decode(res).toString();
	}
	//#endregion
	
	//#region sendMsg(sysCode,token,serviceCode,msg,port,address,resBufferSize)  发送数据到本地，并获取返回值
	/**
	 * 发送数据到本地，并获取返回值
	 * @param sysCode        系统代码
	 * @param token          token
	 * @param serviceCode    服务代码
	 * @param msg            发送消息内容
	 * @param port           目标端口号
	 * @param address        目标地址
	 * @param resBufferSize  接收消息缓存大小
	 * @return               接收到的消息
	 * @throws Exception     异常
	 * 2022年11月29日
	 * @author mbg
	 */
	public String[] sendMsg(String sysCode,String token,String serviceCode,
			String msg,int port,String address,int resBufferSize) throws Exception {
		ByteBuffer res = send(buildSendBuffer(sysCode,token,serviceCode,msg),port,address,resBufferSize);
		if(res==null) {
			return null;
		}
		return parse(res);
	}
	//#endregion
	
	//#region sendMsg(msg,port,resBufferSize) 发送数据到本地，并获取返回值
	/**
	 * 发送数据到本地，并获取返回值
	 * @param msg            发送消息内容
	 * @param port           目标端口号
	 * @param resBufferSize  接收消息缓存大小
	 * @return               接收到的消息
	 * @throws Exception     异常
	 * 2022年11月29日
	 * @author mbg
	 */
	public String sendMsg(String msg,int port,int resBufferSize) throws Exception {
		if(msg==null) {
			return null;
		}
		ByteBuffer res = send(ByteBuffer.wrap(msg.getBytes()),port,null,resBufferSize);
		if(res==null) {
			return null;
		}
		// 获取数据
		res.flip();
		return charset.decode(res).toString();
	}
	//#endregion
	
	//#region sendMsg(sysCode,token,serviceCode,msg,port,resBufferSize) 发送数据到本地，并获取返回值
	/**
	 * 发送数据到本地，并获取返回值
	 * @param sysCode        系统代码
	 * @param token          token
	 * @param serviceCode    服务代码
	 * @param msg            发送消息内容
	 * @param port           目标端口号
	 * @param resBufferSize  接收消息缓存大小
	 * @return               接收到的消息
	 * @throws Exception     异常
	 * 2022年11月29日
	 * @author mbg
	 */
	public String[] sendMsg(String sysCode,String token,String serviceCode,String msg,int port,int resBufferSize) throws Exception {
		ByteBuffer res = send(buildSendBuffer(sysCode,token,serviceCode,msg),port,null,resBufferSize);
		if(res==null) {
			return null;
		}
		return parse(res);
	}
	//#endregion
	
	//#region send(msg,port)  发送消息到本地端口
	/**
	 * 发送消息到本地端口
	 * @param msg         消息内容
	 * @param port        目标端口号
	 * @throws Exception  异常
	 * 2022年11月29日
	 * @author mbg
	 */
	public void send(String msg,int port) throws Exception {
		if(msg==null) {
			return;
		}
		send(ByteBuffer.wrap(msg.getBytes()),port,null,0);
	}
	//#endregion
	
	//#region send(sysCode,token,serviceCode,msg,port) 发送消息到本地端口
	/**
	 * 发送消息到本地端口
	 * @param sysCode     系统代码
	 * @param token       token
	 * @param serviceCode 服务代码
	 * @param msg         消息内容
	 * @param port        目标端口号
	 * @throws Exception  异常
	 * 2022年11月29日
	 * @author mbg
	 */
	public void send(String sysCode,String token,String serviceCode,String msg,int port) throws Exception {
		if(msg==null) {
			return;
		}
		send(buildSendBuffer(sysCode,token,serviceCode,msg),port,null,0);
	}
	//#endregion
	
	//#region send(msg,port,address) 发送消息
	/**
	 * 发送消息
	 * @param msg         消息内容
	 * @param port        目标端口号
	 * @param address     目标地址（为空则发送到本地）
	 * @throws Exception  异常
	 * 2022年11月29日
	 * @author mbg
	 */
	public void send(String msg,int port,String address) throws Exception {
		if(msg==null) {
			return;
		}
		send(ByteBuffer.wrap(msg.getBytes()),port,address,0);
	}
	//#endregion
	
	//#region send(sysCode,token,serviceCode,msg,port,address) 发送消息
	/**
	 * 发送消息
	 * @param sysCode     系统代码
	 * @param token       token
	 * @param serviceCode 服务代码
	 * @param msg         消息内容
	 * @param port        目标端口号
	 * @param address     目标地址（为空则发送到本地）
	 * @throws Exception  异常
	 * 2022年11月29日
	 * @author mbg
	 */
	public void send(String sysCode,String token,String serviceCode,String msg,int port,String address) throws Exception {
		if(sysCode==null || sysCode.length()<1 || token==null 
				|| token.length()<1 || serviceCode==null || serviceCode.length()<1) {
			return;
		}
		send(buildSendBuffer(sysCode,token,serviceCode,msg),port,address,0);
	}
	//#endregion
	
	//#region parse(buffer) 解析服务端返回的数据
	/**
	 * 解析服务端返回的数据
	 * @param buffer 缓存对象
	 * @return       返回的数据 0:msgType[RMZE] 1:sysCode 2:token  3:msgCode 4:msg
	 * 2022年11月30日
	 * @author mbg
	 */
	private String[] parse(ByteBuffer buffer) {
		String[] res = new String[5];              // 构建返回值
		if(buffer==null) {
			return res;
		}
		buffer.flip();
		int        length = buffer.limit();          // 数据长度
		ByteBuffer subBb = ByteBuffer.allocate(length); // 缓存段
		int        level  = 0;                          // 数据位置
		byte       ele;
		for(int i=0;i<length;i++) {
			ele = buffer.get(i);
			if(ele==MSG_SEPARATOR) {
				subBb.flip();
				if(level==0) {
					// 消息类型
					res[0] = charset.decode(subBb).toString();
				}else if(level==1) {
					if("E".equals(res[0])) {
						// error code
						res[3] = charset.decode(subBb).toString();
					}else {
						// sysCode
						res[1] = charset.decode(subBb).toString();
					}
				}else if(level==2) {
					if("E".equals(res[0])) {
						// error msg
						res[4] = charset.decode(subBb).toString();
					}else {
						// token
						res[2] = charset.decode(subBb).toString();
					}
				}else if(level==3) {
					if("E".equals(res[0])) {
						// error msg
						res[1] = charset.decode(subBb).toString();
					}else {
						// msgCode
						res[3] = charset.decode(subBb).toString();
					}
				}
				subBb.clear();
				level ++;
			}else {
				subBb.put(ele);
			}
		}
		if(level==4 && subBb.position()>0) {
			subBb.flip();
			if("E".equals(res[0])) {
				// token
				res[2] = charset.decode(subBb).toString();
			}else {
				// msg
				res[4] = charset.decode(subBb).toString();
			}
		}
		return res;
	}
	//#endregion

	//#region buildSendBuffer(sysCode,token,serviceCode,msg)  拼装发送报文
	/**
	 * 拼装发送报文
	 * @param sysCode     系统代码
	 * @param token       token
	 * @param serviceCode 服务代码
	 * @param msg         发送数据
	 * @return            拼装后的缓存对象
	 * @throws Exception  异常
	 * 2022年11月30日
	 * @author mbg
	 */
	private ByteBuffer buildSendBuffer(String sysCode,String token,String serviceCode,String msg) throws Exception {
		if(sysCode==null || sysCode.length()<1 || token==null 
				|| token.length()<1 || serviceCode==null || serviceCode.length()<1) {
			throw new Exception("The sysCode or token or serviceCode is Empty");
		}
		byte[] sysCodeBytes = sysCode.getBytes();
		byte[] tokenBytes   = token.getBytes();
		byte[] codeBytes    = serviceCode.getBytes();
		int size            = sysCodeBytes.length+1+tokenBytes.length+1+codeBytes.length;
		if(size>MAX_LENGTH) {
			throw new Exception("The Message Size Overflow Then "+MAX_LENGTH);
		}
		boolean hasSetMsg = false; // 是否设置了msg变量
		ByteBuffer buffer = null; // 构建返回值
		if(msg!=null && msg.length()>0) {
			byte[] msgBytes = msg.getBytes();
			if(size+1+msgBytes.length<=MAX_LENGTH) {
				hasSetMsg = true;
				buffer = ByteBuffer.allocate(size+1+msgBytes.length);
				buffer.put(sysCodeBytes);
				buffer.put(MSG_SEPARATOR);
				buffer.put(tokenBytes);
				buffer.put(MSG_SEPARATOR);
				buffer.put(codeBytes);
				buffer.put(MSG_SEPARATOR);
				buffer.put(msgBytes);
			}
		}
		if(!hasSetMsg) {
			buffer = ByteBuffer.allocate(size);
			buffer.put(sysCodeBytes);
			buffer.put(MSG_SEPARATOR);
			buffer.put(tokenBytes);
			buffer.put(MSG_SEPARATOR);
			buffer.put(codeBytes);
		}
		return buffer;
	}
	//#endregion
	
	//#region send(buffer,address,port,resBufferSize) 发送数据
	/**
	 * 发送数据
	 * @param buffer         需要发送的数据
	 * @param port           目标端口
	 * @param address        目标地址 （默认本机地址）
	 * @param resBufferSize  如果需要接收数据（仅一次）将这个值设置为接收数据缓存大小，小于1部接收数据
	 * @throws Exception     异常
	 * 2022年11月29日
	 * @author mbg
	 */
	public ByteBuffer send(ByteBuffer buffer,int port,String address,int resBufferSize) throws Exception {
		if(channel==null) {
			channel = DatagramChannel.open(); // 通信类
			channel.configureBlocking(false);
		}
		if(address==null || address.length()<1) {
			address = "127.0.0.1";
		}
		Selector selector = null;
		if(resBufferSize>0) {
			selector = Selector.open(); // 选择器
			channel.register(selector,SelectionKey.OP_READ); 
		}
		buffer.flip(); // 缓存添加结束
		channel.send(buffer,new InetSocketAddress(address,port)); 
		if(resBufferSize>0) {
			ByteBuffer resBuffer = ByteBuffer.allocateDirect(resBufferSize);
			if(selector.select()>0) {
				Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
				SelectionKey key;
				while(selectedKeys.hasNext()) {
					key = selectedKeys.next();
					selectedKeys.remove();
					if(key.isReadable()) {
						channel.receive(resBuffer); 
					}
				}
			}
			return resBuffer;
		}
		return null;
	}
	//#endregion
}




